﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using blackhouse.BlackApart.Business.Apartments;
using blackhouse.BlackApart.Business.Discounts;
using blackhouse.BlackApart.Business.ApartmentServices;

namespace blackhouse.BlackApart.Business.Reservations {
    public class FreeApartments : IEnumerable<Variant> {

        #region Fields

        private int apartmentTypeId;
        private IEnumerable<Variant> variants;

        #endregion

        #region Properties

        public DateTime ArrivalDate { get; private set; }
        public DateTime DepartureDate { get; private set; }
        public int ForPeopleCount { get; private set; }
        public bool AllowVariousApartmentTypes { get; private set; }
        public bool AllowChangeInLocation { get; private set; }

        #endregion

        #region Constructors

        public FreeApartments(DateTime arrivalDate, DateTime departureDate, int forPeoplesCount) : this(arrivalDate, departureDate, forPeoplesCount, false, false) { }

        public FreeApartments(DateTime arrivalDate, DateTime departureDate, int forPeoplesCount, bool allowVariousApartmentTypes, bool allowChangeInLocation) {
            this.AllowVariousApartmentTypes = allowVariousApartmentTypes;
            this.AllowChangeInLocation = allowChangeInLocation;
            this.SetDates(arrivalDate, departureDate);
            this.SetForPeoples(forPeoplesCount);
        }

        #endregion

        #region Methods

        public void FilterToApartmentType(int apartmentTypeId) {
            if (this.apartmentTypeId != apartmentTypeId) {
                this.variants = null;
                this.apartmentTypeId = apartmentTypeId;
            }
        }

        private void SetDates(DateTime arrivalDate, DateTime departureDate) {
            if (arrivalDate.Date < DateTime.Now.Date) throw new ExceptionAparts(2, null, new KeyValuePair<string, object>("arrivalDate", arrivalDate));
            if (arrivalDate.Date >= departureDate) throw new ExceptionAparts(3, null, new KeyValuePair<string, object>("arrivalDate", arrivalDate), new KeyValuePair<string, object>("departureDate", departureDate));
            this.ArrivalDate = arrivalDate.Date.AddHours(14);
            this.DepartureDate = departureDate.Date.AddHours(12);
        }

        private void SetForPeoples(int count) {
            if (count <= 0 || count > 10000) throw new ExceptionAparts(4, null, new KeyValuePair<string, object>("numberOfPeople", count));
            this.ForPeopleCount = count;
        }

        private IEnumerable<Variant> Find() {
            Factory.Instance().GetWebReservation(0).RemoveOpenReservation();
            IEnumerable<Variant> variants = this.FindVariants();
            DiscountProvider discounts = new DiscountProvider(this.ArrivalDate, this.DepartureDate, this.ForPeopleCount);
            foreach (Variant variant in variants) {
                discounts.Apply(variant);
            }
            return variants;
        }

        private IEnumerable<Variant> FindVariants() {
            IApartment baseApartment = Factory.Instance().GetApartment();
            IApartment[] apartments;
            if (this.apartmentTypeId == 0)
                apartments = baseApartment.GetAllFreeApartments(this.ArrivalDate, this.DepartureDate);
            else
                apartments = baseApartment.GetAllFreeApartments(this.ArrivalDate, this.DepartureDate, this.apartmentTypeId);

            List<Variant> variants = new List<Variant>();
            foreach (IApartmentGroup group in apartments.Select(a => a.Group).Distinct()) {
                if (this.apartmentTypeId != 0 && this.apartmentTypeId != group.Id) continue;
                Variant v = null;
                if (group.NumberOfPeople == this.ForPeopleCount - 1) {
                    IAdditionalService extraBedService = Factory.Instance().GetAdditionalService().FindService(group.Id, "extrabed");
                    if (extraBedService != null) {
                        v = new Variant();
                        v.AddItem(apartments.First(a => a.Group.Equals(group)), this.ArrivalDate, this.DepartureDate, this.ForPeopleCount, extraBedService);
                        variants.Add(v);
                        continue;
                    }
                }
                v = this.CombineReservationForGroup(apartments.Where(a => a.Group.Equals(group)).ToList(), this.ForPeopleCount, this.ArrivalDate, this.DepartureDate);
                if (v.Items.Length > 0)
                    variants.Add(v);
            }
            var q = from vs in variants
                    select vs;
            if (!this.AllowChangeInLocation)
                q = from vs in q where !vs.ItIsChangeInLocation select vs;
            if (!this.AllowVariousApartmentTypes)
                q = from vs in q where !vs.AreVariousApartmentTypes select vs;
            return q;
        }

        private Variant CombineReservationForGroup(List<IApartment> apartmentWithGroup, int numberOfPeoples, DateTime arrivalDate, DateTime departureDate) {
            Variant v = new Variant();
            if (apartmentWithGroup.Count() == 0) return v;
            if (apartmentWithGroup.Count() == 1) {
                if (apartmentWithGroup[0].NumberOfPeople >= numberOfPeoples)
                    v.AddItem(apartmentWithGroup[0], arrivalDate, departureDate, numberOfPeoples);
                return v;
            }
            int[] apartmentsSize = apartmentWithGroup.Select(a => a.NumberOfPeople).ToArray();
            if (apartmentsSize.Contains(numberOfPeoples)) {
                IApartment ap = apartmentWithGroup.FirstOrDefault(a => a.NumberOfPeople == numberOfPeoples);
                v.AddItem(ap, arrivalDate, departureDate, numberOfPeoples);
                return v;
            }
            if (apartmentsSize.Any(s => s > numberOfPeoples)) {
                int rApSize = 0;
                foreach (int aps in apartmentsSize.OrderBy(s => s)) {
                    if (aps > numberOfPeoples) {
                        rApSize = aps;
                        break;
                    }
                }
                IApartment ap = apartmentWithGroup.FirstOrDefault(a => a.NumberOfPeople == rApSize);
                v.AddItem(ap, arrivalDate, departureDate, numberOfPeoples);
                return v;
            }

            for (int xa = 2; xa < apartmentsSize.Length; xa++) {

                List<CombineReservationInnerHelper> reservations = new List<CombineReservationInnerHelper>();

                ArrayCombinations ac = new ArrayCombinations(apartmentsSize.Length, xa);
                while (ac.Next()) {
                    CombineReservationInnerHelper cr = new CombineReservationInnerHelper();
                    cr.ApartmentsSize = new int[xa];
                    for (int xcr = 0; xcr < ac.Combination.Length; xcr++) {
                        cr.ApartmentsSize[xcr] = apartmentsSize[ac.Combination[xcr]];
                    }
                    cr.Sum = cr.ApartmentsSize.Sum();
                    if (cr.Sum >= numberOfPeoples) {
                        cr.Avg = CalculateReservationAvg(cr.ApartmentsSize);
                        reservations.Add(cr);
                    }
                }
                if (reservations.Count == 0) continue;
                List<CombineReservationInnerHelper> reservations2 = reservations.Where(r => r.Sum == numberOfPeoples).ToList();
                CombineReservationInnerHelper goodFind = null;
                if (reservations2.Count == 1) goodFind = reservations2[0];
                if (reservations2.Count > 1) goodFind = reservations2.OrderBy(cr => cr.Avg).First();
                if (goodFind == null)
                    goodFind = reservations.OrderBy(r => r.Sum).ThenBy(r => r.Avg).First();

                ApartmentUserReservation[] apres = new ApartmentUserReservation[goodFind.ApartmentsSize.Length];
                for (int xaa = 0; xaa < goodFind.ApartmentsSize.Length; xaa++) {
                    IApartment apart = apartmentWithGroup.FirstOrDefault(a => a.NumberOfPeople == goodFind.ApartmentsSize[xaa] && !apres.Where(aur => aur.Apartment != null).Any(aur => aur.Apartment.Id == a.Id));
                    apres[xaa] = new ApartmentUserReservation() { Apartment = apart, NumberOfPeople = apart.NumberOfPeople };
                }
                foreach (ApartmentUserReservation apre in apres) {
                    if (apre.NumberOfPeople < numberOfPeoples) {
                        v.AddItem(apre.Apartment, arrivalDate, departureDate, apre.NumberOfPeople);
                        numberOfPeoples -= apre.NumberOfPeople;
                    }
                    else {
                        if (numberOfPeoples > 0) {
                            v.AddItem(apre.Apartment, arrivalDate, departureDate, numberOfPeoples);
                        }
                    }
                }
                return v;

            }
            return v;
        }

        private double CalculateReservationAvg(int[] ar) {
            double avg = ar.Average();
            double diference = 0;
            for (int arl = 0; arl < ar.Length; arl++) {
                diference += Math.Abs(ar[arl] - avg);
            }
            return diference;
        }

        public IEnumerator<Variant> GetEnumerator() {
            if (this.variants == null)
                this.variants = this.Find();
            return this.variants.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
            return this.GetEnumerator();
        }

        #endregion

    }
}
